package com.ruoyi.matrix.queue

import cn.hutool.system.HostInfo
import com.ruoyi.common.utils.spring.SpringUtils
import com.ruoyi.matrix.dim.EnvDim
import org.springframework.data.redis.core.StringRedisTemplate
import org.springframework.data.redis.core.ZSetOperations

/**
 * 简单的通用队列
 */
class SimpleQueue(private val env: String, private val queue: String) {
    private val fatmoreLock get() = SpringUtils.getBean<FatmoreLock>("fatmoreLock")
    private val redisTemplate get() = SpringUtils.getBean(StringRedisTemplate::class.java)
    private val hostname = HostInfo().name

    val name: String get() = queue
    val size: Long get() = redisTemplate.opsForZSet().zCard(join(env, queue)) ?: 0L

    // order enqueue/dequeue/remove/delay(re-enqueue)
    fun add(value: String, delay: Long = 1L): Boolean = add(queue, value, System.currentTimeMillis() + delay)

    fun adds(values: List<String>, delay: Long = 1L): Int {
        if (values.size == 1) return if (add(values.first(), delay)) 1 else 0
        return addMultiplePipeline(queue, values, System.currentTimeMillis(), delay)
    }

    fun pick(): QueueMember? = frontOf(queue)

    fun remove(value: String) = remove(queue, value).also { resetProcessTimes(value) }

    fun removeWithKey(value: String, key: String) = remove(queue, value).also { resetProcessTimes(key) }

    fun delay(value: String, delay: Long): Boolean = update(queue, value, System.currentTimeMillis() + delay)

    fun delayAt(value: String, ts: Long): Boolean = update(queue, value, ts)

    // times and lock for a key
    fun resetProcessTimes(key: String) = fatmoreLock.resetForIncrease(join(env, "times.${queue}.${key}"))

    fun incProcessTimes(key: String): Long = fatmoreLock.increase(join(env, "times.${queue}.${key}"))

    fun getProcessTimes(key: String): Long = fatmoreLock.getBeforeIncrease(join(env, "times.${queue}.${key}"))

    fun lock(key: String, timeout: Long): Boolean =
        fatmoreLock.lock(join(env, "lock.${queue}.${key}"), hostname, timeout)

    fun lock(key: String, by: String, timeout: Long): Boolean =
        fatmoreLock.lock(join(env, "lock.${queue}.${key}"), by, timeout)

    fun unlock(key: String) = fatmoreLock.unlock(join(env, "lock.${queue}.${key}"))

    fun removeAndUnlock(value: String) = remove(value).also { unlock(value) }

    fun removeAndUnlockWithKey(value: String, key: String) = remove(value).also { unlock(key) }

    // private functions

    private fun add(queue: String, value: String, score: Long): Boolean {
        val stringZSetOperations: ZSetOperations<String, String> = redisTemplate.opsForZSet()
        return stringZSetOperations.add(join(env, queue), value, score.toDouble()) ?: false
    }

    private fun update(queue: String, value: String, score: Long): Boolean = add(queue, value, score)

    /** 批量入队，每个延迟${delay}毫秒 */
    private fun addMultiplePipeline(queue: String, values: List<String>, score: Long, delay: Long = 1L): Int {
        val scoreDouble = score.toDouble()
        return redisTemplate.executePipelined { conn ->
            values.forEachIndexed { index, it ->
                conn.zSetCommands()
                    .zAdd(join(env, queue).toByteArray(), scoreDouble + ((index + 1) * delay), it.toByteArray())
            }
            null
        }.size
    }

    private fun frontOf(queue: String): QueueMember? {
        val objSet = zRangeWithScores(queue, 0, 0)
        return if (objSet.isEmpty()) {
            null
        } else {
            val zsetObj = objSet.toTypedArray()[0]
            val qm = QueueMember()
            qm.member = zsetObj.value
            qm.score = zsetObj.score
            qm
        }
    }

    private fun zRangeWithScores(queue: String, start: Long, end: Long): Set<ZSetOperations.TypedTuple<String>> {
        return redisTemplate.opsForZSet().rangeWithScores(join(env, queue), start, end)!!
    }

    private fun remove(queue: String, value: String) {
        redisTemplate.opsForZSet().remove(join(env, queue), value)
    }

    // 开发环境用私人hostname隔离
    private fun join(env: String, queue: String): String {
        return if (env == EnvDim.prod) "${env}.${queue}"
        else "${env}.${hostname}.${queue}"
    }
}